feat(dashboard): make the live board actionable - #67
Conversation
Records what was verified by building and running the app on 2026-09-05: the dashboard renders and the control plane executes runs, but nothing feeds it, nothing refreshes it, and starting it needs a database. The plan restructures the edge rather than the packages, in five phases with a demonstrable exit criterion each. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seeing the dashboard required Postgres, a migration, a signing secret, and control-plane tokens before the first page could render, so the quickest way to look at it was not to. `dev:solo` is `nuxt dev --dotenv .env.solo`: the same app, the same router, and the same `/api/auth/**` endpoints, with Better Auth on the in-memory store the Playwright preview server already uses. `.env.solo` is checked in because it holds nothing worth keeping out of the repository — the session store dies with the process, and the control-plane token is only accepted by a server started this way. It is loaded only when a command names it with `--dotenv`, so `.env` and every deployment are untouched. Verified: `turbo run dev:solo --filter=@code-zero/dashboard` from a checkout with no database — `/api/v1/health` 200, `/login` 200, `POST /api/auth/sign-up/email` returns a session, and `/` renders Control Plane with that cookie. check:repo, format:check, and typecheck pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The trail had two homes it did not need. Recording went through a hand-rolled `AuditRecorder` injected into the RPC context, while `evlog` — already installed, already wrapping both transports — has an audit pipeline with the same shape. Reading lived in a Nitro route outside the router, written when the router could not authenticate a browser session; since it can, that reason is gone. Recording is now `log.audit()` / `log.audit.deny()`. The persisted record is evlog's own `AuditFields` plus a storage id and timestamp, so there is no second audit vocabulary to keep in step, and `auditEnricher` fills in request context (`requestId`, `traceId`, ip, user agent) no call site had been passing. The identity is evlog's deterministic `idempotencyKey`, so a retried delivery lands on the key it already wrote instead of appending a second copy. `auditLogPlugins` carries the record to the same KV-backed store as before, filtered by `auditOnly` and awaited so an audited mutation cannot answer 200 and lose its record, and installed as `EvlogHandlerPlugin` plugins rather than as its `drain`, so a deployment's own request logging is untouched. Reading is `audit.list`, an authenticated procedure gated on a new explicit `Principal.admin` rather than on a mode grant: what a caller may run and what a caller may see are different questions. Operator tokens are never administrators — the trail records their use, so letting one read it back would let a token audit itself. Verified against the built server: `tasks.create` success and denial each persisted one record carrying `context.requestId` and the user agent; `audit.list` answered FORBIDDEN for an operator token and for a signed-in non-admin over `/rpc/**`. lint:ci, typecheck, and 151 api tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The board showed whatever the last fetch caught. A run records its lifecycle events as it works, so a task appeared and then sat at the state it had when the page loaded until someone pressed refresh — and a stalled page looked exactly like a quiet one. `/api/events` streams the overview over SSE, behind the same session the page needs, pushing whenever a task record is written. The store wrapper that emits lives in the composition root rather than in `packages/api`, so the store contract stays plain persistence: every writer — the router, the webhook route, and the run recording its own events — already goes through that one instance, so a subscriber sees the whole lifecycle rather than the transitions one transport happens to see. Writes are coalesced over 250ms, so a run that records ten events in a burst sends one overview. The client writes each message straight into the query cache the page already reads, rather than invalidating and asking the server for what it just sent. The query stays the loader for the first paint and for a client whose stream never opens. A header indicator says whether the board is actually following, because a stalled stream is otherwise indistinguishable from an idle one. `useLiveOverview` takes the query key rather than reaching for `useNuxtApp()`, which is also what keeps it out of the Nuxt runtime for the unit suite. Verified against the built server: the stream answers 401 unauthenticated; with a session it pushes the current overview on connect and again when a task created through `/api/v1/tasks` reached the store. lint:ci, typecheck, i18n:report, and the dashboard suites pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The router has accepted `tasks.create` and `approvals.decide` since the control plane existed, but nothing in the UI called either: the board could watch a run stop for a human decision and offer no way to give one. The sidebar meanwhile listed nine sections that were inert buttons — a nav that promises surfaces the app does not have teaches an operator that clicking does nothing. The inspector offers Approve and Reject with an optional comment while a task is `needs-human` and undecided, and shows the recorded decision once one exists, because the control plane accepts exactly one. A `details`-based form queues a task from the header. Both emit rather than mutate: the page owns the typed client and the one place a failure is surfaced, so there is no second path to keep in step. Neither writes into the query cache — the decision lands in the store, and the store is what the live stream pushes back, so the board updates from the same source every client sees rather than from a guess about what the server did. The repository is typed rather than picked from the allow-list: the list is server-side checkout paths, which the persisted records deliberately keep out of reach, and `tasks.create` already names the rule it refused on. Verified against the built server, over the same `/rpc/**` the page uses: a session created a task, and got FORBIDDEN for a repository outside the allow-list and for a mode a session may not request. The approval and form logic are covered by 13 new component tests. Browser verification of the rendered result was not possible — this environment has no automation host — so the visual pass against `nuxt-frontend-review` is still owed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Work only reached the control plane when a provider delivered it, so a self-hosted deployment behind no public URL had nothing feeding it: the board stayed empty unless someone posted a task by hand. `server/plugins/poller.ts` lists each watched repository's open pull requests on an interval and starts a review for every head commit it has not started one for. It is the pull-based half of the job the webhook route already does, and it shares that route's durable `DeliveryClaimStore`, so a commit reviewed through one path is never reviewed again through the other. The claim key carries the head sha, which is what makes a new push earn a new review and an unchanged pull request earn nothing. Constraints that are enforced, not documented: it is off unless `CODE_ZERO_POLL_REPOSITORIES` names something; it requests only `observe` or `suggest`, so work nobody asked for cannot write to a checkout; and the checkout comes from the path an operator paired with the repository rather than being derived from the provider's answer, so a run can never target somewhere nobody named. A failed start releases its claim so the next pass retries, and one unreachable provider does not end the pass. `listOpenPullRequests` is new on the GitHub adapter and returns both the base and head commits, because a review reads the diff between them. It skips a record missing either rather than losing the page it arrived in. Verified against the built server: silent and healthy when unconfigured; refuses to start naming the missing variable when configured without a token; and with a token it reports the repository that failed without stopping the process or putting the credential in the log. A pass against real GitHub is still owed — this environment has no credentials, and the tests deliberately reach no network. 27 new tests; lint:ci, typecheck across the graph, and the build pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`accessFromEnvironment` returned `undefined` unless `CODE_ZERO_CONTROL_PLANE_TOKENS` was set, and `mayTargetRepository` fails closed without a policy. So a deployment that authenticates only browser sessions could never create a task: the `CODE_ZERO_CONTROL_PLANE_REPOSITORIES` it had configured did not exist as far as the router was concerned, and every target was refused. The two variables answer different questions. Tokens say who a machine caller is; the allow-list says what any authenticated caller may target, including a person signed into the dashboard. Either one now produces a policy, and only neither returns `undefined`, so an unconfigured deployment still rejects every mutation and a deployment with no tokens still authenticates no machine caller — `principals` is simply empty. Found by running the dashboard with a session and an allow-list and nothing else, which is what `dev:solo` and a self-hosted single-owner install both look like. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A local `zero run` and the dashboard kept separate histories: the CLI executed in the checkout and recorded nothing the board could read, so work started from a terminal was invisible to the surface built to watch it. `--remote` hands the run to a deployment's control plane instead. It presents the session `zero login` stored as a bearer token, so the run is attributed to the person who signed in rather than to a shared operator token, and it goes to `/rpc/**` because that is the only transport that resolves a session — stating the `Sec-Fetch-Mode` header its CSRF guard reads, which a browser sends on its own. The deployment therefore needs `AUTH_ENABLE_DEVICE_AUTHORIZATION=true`, the same flag `zero login` already requires. A flag rather than an inference from `CODE_ZERO_URL`: that variable already selects which deployment `login` and `logout` act on, so treating its presence as "run somewhere else" would silently move an operator's run to another machine and another checkout the first time they set it. The plan called for the implicit form; this is the deliberate departure from it. The exit code comes from the same table a local run uses, so CI reads either the same way, and an answer that is not a result is refused rather than allowed to exit 0. Verified against the built server end to end: from a checkout, `zero run --proactive --remote --json` authenticated with a stored session, executed on the deployment, printed the result, exited 0, and the task appeared in the board's own `dashboard.overview`. 14 new tests; lint:ci and typecheck pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The whole live board rests on one property — every task write announces itself — and nothing tested it. It could not be tested either: the notification was a subclass of the KV-backed store, so reaching it meant reaching the deployment's filesystem driver. `observeWrites` is that subclass turned into a decorator over any `TaskStore`, so a test drives it against an in-memory one. The tests state the parts that matter: it announces every write, only after the write landed, and says nothing when the write failed — a listener re-reading the store on a failed write would find nothing changed and a listener told too early would read the previous state. Forwarding `clear` went with it: `PersistentTaskStore` has none, so it was a capability the wrapper invented for nobody. `docs/architecture.md` gains the live-state paragraph the plan asked for, and `docs/PLAN.md` records what was built, the three places the plan was departed from and why, and the five things still owed — the browser review among them. Verified: 997 tests across every package and app, lint:ci, typecheck, check:repo, i18n:report, and the build all pass. The docs build needs a larger heap than this sandbox allows by default and passes with one; nothing in this branch touches it beyond a one-line table. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Add the `code` executable and VS Code CLI archive
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR adds multiple production capabilities—live SSE updates, task and approval workflows, pull-request polling, remote execution, persistence, and deployment-policy changes—alongside authentication and authorization modifications. Its broad runtime and schema impact, particularly in sensitive auth/control-plane paths, warrants human review. Not approved because:
Review your spending limits in Billing settings. You can add or adjust custom eligibility rules. Learn more. |
- Add deployment YAML configuration for control-plane policy - Store repository allow-lists in the database - Add standalone dashboard, docs, and marketing dev commands
- Track the latest aube release in mise - Add config and database workspace dependencies to the lockfile
…t the environment The repository and mode grants moved out of the environment in the previous commit but nothing was rewired to the new sources, so the dashboard imported two symbols `@code-zero/api` no longer exports. This closes that. Both oRPC transports now resolve the operator tokens through `controlPlaneAccess()` and answer "may a run target this checkout?" from the `repository` table per request, so a repository configured a moment ago is usable without a restart. `/api/v1/**` takes its CORS allow-list from `control_plane.origins`. The poller reads its watched repositories from the same table on every pass and runs each in the mode that repository is configured with, rather than one mode for the whole deployment. It reschedules itself when a pass finishes instead of holding a fixed interval, which reads `poll.interval_seconds` each time and removes the guard against a pass overrunning its own interval. Five variables are gone: CODE_ZERO_CONTROL_PLANE_REPOSITORIES, _MODES and _ORIGINS, CODE_ZERO_POLL_REPOSITORIES, _INTERVAL_SECONDS and _MODE. What stays in the environment is what a deployment already keeps there: the operator tokens, and CODE_ZERO_CONFIG to find the rest. Also fixes the schema test, which the new `repository` table left failing, and three type assertions the type-aware lint refuses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…st discovery `listOpenPullRequests` referenced `MAX_PAGES` in its pagination loop without declaring it, so every call threw `ReferenceError: MAX_PAGES is not defined` and broke polling entirely. Adds the constant (20 pages, 2,000 pull requests) and rewrites the tests that covered the old single-page `perPage` parameter to cover walking multiple pages and stopping at the cap instead.
isTaskResult only checked that id/state were strings, so a queued or
in-progress /rpc response (e.g. { id, state: 'queued' }) passed as a
completed TaskResult. JSON mode then printed it and exited 0 before the
review had actually finished, and normal-mode output threw while rendering
a plan that was never populated. Now requires state to be one of the three
terminal states and plan to be present.
isAdministrator compared user.role for strict equality against the admin role, but Better Auth stores multiple roles as one comma-separated string. The deleted audit-logs.get.ts route split and checked membership; the new audit.list and repositories.* procedures route through isAdministrator instead, so an account provisioned as e.g. "support,admin" silently lost access to both.
controlPlaneAccess() cached its promise with `pending ??= ...`, so once deploymentConfig() rejected once, pending stayed set to that same rejection forever — every request needing a control-plane token or mode grant would fail the same way until the process was restarted. The cache now clears itself on rejection so the next call retries instead of replaying it. Extracted createControlPlaneAccessCache so the recovery behavior is covered by a plain-Node test, the same reason createOverviewBroadcaster takes its dependencies as parameters.
RepositoryInput marks provider/owner/name/mode/pollEnabled optional, but saveRepository's upsert always wrote values with defaults substituted in for every omitted field. Updating just one field of an existing repository (a corrected checkoutPath, say) silently reset its mode back to 'observe' and turned polling off. The update clause now only names the fields the caller actually sent, leaving the rest of the row untouched.
memoryRepositoryStore seeded checkoutPath verbatim from CODE_ZERO_SOLO_REPOSITORIES, while every other write path (repositories.save, mayTargetRepository) resolves it first. A relative or trailing-slash entry in dev:solo's env file would never string-equal the resolved path a task creation checks it against, silently refusing every task for that repository.
code and vscode_cli.tar.gz (~46MB together) were committed by 06968df, unrelated to this feature and not referenced by any build, script, or CI step. Removed going forward and gitignored so a future checkout doesn't carry a stray editor download. The blobs still exist in this branch's history prior to this commit; a full purge would need a history rewrite (git filter-repo + force-push), which is a separate, coordinated action best done deliberately rather than as part of this fix.
◈ PR Lens
Architecture 19 components touched across 8 lanes. Inside the changed components — 3 viewsComponent view — Live state and overview streaming Live dashboard updates streamed over Server-Sent Events from process-local store write notifications. Component view — Polling and repository administration Pull-based PR review poller and database-backed repository allow-list management. Component view — Control-plane access and audit pipeline Deployment policy configuration, bearer authentication, and unified evlog audit pipeline. Data flow
The other flows — 2 sequences
View
Tip Run 🪧 More tips
Thanks for using PR Lens! It's built by Coldtea, free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
CODE_ZERO_CONTROL_PLANE_REPOSITORIES and CODE_ZERO_CONTROL_PLANE_MODES are no longer read anywhere; repository allow-listing is DB-backed and mode grants come from packages/config. AGENTS.md still pinned aube at 1.38.0 while package.json/mise.toml/CI use 1.41.0.
The unqualified restore-keys fallback let a cache miss on the exact lockfile hash seed node_modules/.aube from a prior, incompatible lockfile state, leaving dangling symlinks (drizzle-orm under @better-auth/drizzle-adapter) that broke the dashboard Nitro build with ENOENT. Drop the fallback so a miss forces a clean install.
| | Variable | Purpose | | ||
| | --------------------------------- | -------------------------------------------------------------------------------------------- | | ||
| | `CODE_ZERO_CONTROL_PLANE_TOKENS` | Comma-separated `name:token` bearer credentials | | ||
| | `CODE_ZERO_CONTROL_PLANE_ORIGINS` | Comma-separated origins allowed to read `/api/v1/**` cross-origin via CORS; empty by default | |
There was a problem hiding this comment.
Update CORS Configuration Docs
This table tells operators to configure CODE_ZERO_CONTROL_PLANE_ORIGINS, but the service no longer reads that variable. Allowed origins now come from control_plane.origins in code-zero.deployment.yml, so following this setup leaves cross-origin /api/v1/** reads unavailable. The endpoint-protection guide repeats the obsolete setting, creating the same failed browser integration path there.
Artifacts
- The authored Node runtime harness loads deployment configuration and invokes the installed production CORS plugin for the `/api/v1/health` service contract, showing the environment variable is ignored and YAML origins are honored.
- Executed `node trex-artifacts/cors-policy-validation.mjs before` with the documented origin environment variable set; the HTTP 200 response has no allow-origin header, proving the variable does not enable CORS.
- Executed `node trex-artifacts/cors-policy-validation.mjs after` with the same origin in `control_plane.origins`; the HTTP 200 response returns the matching allow-origin header, proving deployment YAML controls CORS.
Ran code and verified through T-Rex
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/docs/content/1.guide/4.environment-variables.md
Line: 57
Comment:
**Update CORS Configuration Docs**
This table tells operators to configure `CODE_ZERO_CONTROL_PLANE_ORIGINS`, but the service no longer reads that variable. Allowed origins now come from `control_plane.origins` in `code-zero.deployment.yml`, so following this setup leaves cross-origin `/api/v1/**` reads unavailable. The endpoint-protection guide repeats the obsolete setting, creating the same failed browser integration path there.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Valid, fixed in c51178d. CODE_ZERO_CONTROL_PLANE_ORIGINS is indeed dead — the CORS plugin in apps/dashboard/server/api/v1/[...].ts reads (await deploymentConfig()).controlPlane.origins. Updated all four pages that still documented the retired variables, not just this row: the env-var table now lists only CODE_ZERO_CONTROL_PLANE_TOKENS and CODE_ZERO_CONFIG and says where the rest of the policy lives, and Protect endpoints / Permissions / the deployment checklist now point at control_plane.origins and control_plane.modes in code-zero.deployment.yml and at the repository table for the allow-list.
This reverts commit e629b6c.
Four dependency edges in the better-auth@1.6.26 subgraph named a peer-qualified package id whose nested suffix was cut one level short — prisma@7.9.1 instead of prisma@7.9.1(better-sqlite3@12.11.1) — so they pointed at snapshots that do not exist. aube hashes the id as written, so `aube ci` linked node_modules/.aube/@better-auth+drizzle-adapter@1.6.26_*/node_modules/drizzle-orm at a virtual-store directory it never materialises, and the dashboard Nitro build failed with ENOENT in the node-externals plugin. Repointed the four edges at the peer-qualified snapshots already in the lockfile. A clean `aube ci` now reproduces the lockfile byte for byte and leaves no dangling links; `aube run build --filter=@code-zero/dashboard` succeeds.
Every field on RepositoryInput is optional, and saveRepository's upsert writes only the ones a caller names. The in-memory store used by AUTH_E2E_MEMORY and dev:solo instead rebuilt the record from defaults, so saving an existing repository with only its checkoutPath demoted a watched acme/widget to an unwatched repository with no coordinates and polling stopped discovering its pull requests. The two stores now answer the same way: an omitted field keeps the existing record's value and falls back to the column default only when there is no existing record.
CODE_ZERO_CONTROL_PLANE_ORIGINS is no longer read: allowed CORS origins come from control_plane.origins in code-zero.deployment.yml, and mode grants from control_plane.modes in the same file, while the repositories tasks.create may target are rows in the store. Four guide pages still told operators to set variables nothing reads, which leaves cross-origin /api/v1/** reads unavailable for anyone following them.
Summary
zero run --remotefor dispatching work to a deployment's control plane.dev:solofor running the dashboard with in-memory authentication and no Postgres.Why
This makes the dashboard a usable control surface instead of a read-only snapshot. Operators can see control-plane activity as it happens, create work, and resolve approval requests from the same interface.
The changes preserve the repository boundaries: the dashboard composes the API and authentication layers, runtime execution remains behind the runner, and remote CLI runs are submitted to the deployment control plane rather than executed locally. Polling provides a webhook-independent path for discovering pull requests while sharing durable delivery claims with webhook processing.
Verification
aube run check:repoaube run lint:ciaube run typecheckaube testaube run buildSafety and compatibility
observemode as read-only, or explained the policy change above.Agent context
Reviewer notes
The change spans the dashboard control loop, audit routing, remote CLI execution, pull-request polling, and solo development setup. Particular attention is warranted for authentication and authorization behavior, polling delivery claims, remote-run repository allow-listing, and the restriction of unattended polling to non-writable modes.
Base branch: main
Confidence Score: 5/5
Safe to merge.
No new actionable findings remain. Previously reported issues are fixed in the current code and documentation, including preservation of omitted in-memory repository fields, deployment-policy documentation, shared pull-request delivery claims, paginated pull-request discovery, terminal remote-result validation, live-stream limits, stale-overview prevention, normalized repository paths, completed review-claim retention, explicit error flags, pinned toolchain versions, and current command references.
Reviews (17): Last reviewed commit: "docs: point control-plane policy at the ..." | Re-trigger Greptile
Context used: